package com.googlecode.stateless4j;

import com.googlecode.stateless4j.delegates.Func;
import com.googlecode.stateless4j.delegates.Func2;
import com.googlecode.stateless4j.delegates.Func3;
import com.googlecode.stateless4j.delegates.Func4;
import com.googlecode.stateless4j.transitions.TransitioningTriggerBehaviour;
import com.googlecode.stateless4j.triggers.DynamicTriggerBehaviour;
import com.googlecode.stateless4j.triggers.IgnoredTriggerBehaviour;
import com.googlecode.stateless4j.triggers.TriggerWithParameters1;
import com.googlecode.stateless4j.triggers.TriggerWithParameters2;
import com.googlecode.stateless4j.triggers.TriggerWithParameters3;
import com.googlecode.stateless4j.validation.ArgumentValidator;

/**
 * State Configuration
 *
 * @param <TState>
 * @param <TTrigger>
 */
public class StateConfiguration<TState, TTrigger>
{
    private final StateRepresentation<TState, TTrigger> representation;
    private final Func2<TState, StateRepresentation<TState, TTrigger>> lookup;
    private final Func<Boolean> noGuard = new Func<Boolean>()
    {

        public Boolean call()
        {
            return true;
        }
    };

    /**
     * @param representation
     * @param lookup
     */
    StateConfiguration(StateRepresentation<TState, TTrigger> representation, Func2<TState, StateRepresentation<TState, TTrigger>> lookup)
    {
        this.representation = ArgumentValidator.argumentNotNull(representation, "representation");
        this.lookup = ArgumentValidator.argumentNotNull(lookup, "lookup");
    }

    /// <summary>
    /// Accept the specified trigger and transition to the destination state.
    /// </summary>
    /// <param name="trigger">The accepted trigger.</param>
    /// <param name="destinationState">The state that the trigger will cause a
    /// transition to.</param>
    /// <returns>The reciever.</returns>
    public StateConfiguration<TState, TTrigger> permit(TTrigger trigger, TState destinationState)
    {
        enforceNotIdentityTransition(destinationState);
        return publicPermit(trigger, destinationState);
    }

    /// <summary>
    /// Accept the specified trigger and transition to the destination state.
    /// </summary>
    /// <param name="trigger">The accepted trigger.</param>
    /// <param name="destinationState">The state that the trigger will cause a
    /// transition to.</param>
    /// <param name="guard">Function that must return true in order for the
    /// trigger to be accepted.</param>
    /// <returns>The reciever.</returns>
    public StateConfiguration<TState, TTrigger> permitIf(TTrigger trigger, TState destinationState,
        Func<Boolean> guard)
    {
        enforceNotIdentityTransition(destinationState);
        return publicPermitIf(trigger, destinationState, guard);
    }

    /// <summary>
    /// Accept the specified trigger, execute exit actions and re-execute entry actions.
    /// Reentry behaves as though the configured state transitions to an identical sibling state.
    /// </summary>
    /// <param name="trigger">The accepted trigger.</param>
    /// <returns>The reciever.</returns>
    /// <remarks>
    /// Applies to the current state only. Will not re-execute superstate actions, or
    /// cause actions to execute transitioning between super- and sub-states.
    /// </remarks>
    public StateConfiguration<TState, TTrigger> permitReentry(TTrigger trigger)
    {
        return publicPermit(trigger, representation.getUnderlyingState());
    }

    /// <summary>
    /// Accept the specified trigger, execute exit actions and re-execute entry actions.
    /// Reentry behaves as though the configured state transitions to an identical sibling state.
    /// </summary>
    /// <param name="trigger">The accepted trigger.</param>
    /// <param name="guard">Function that must return true in order for the
    /// trigger to be accepted.</param>
    /// <returns>The reciever.</returns>
    /// <remarks>
    /// Applies to the current state only. Will not re-execute superstate actions, or
    /// cause actions to execute transitioning between super- and sub-states.
    /// </remarks>
    public StateConfiguration<TState, TTrigger> permitReentryIf(TTrigger trigger, Func<Boolean> guard)
    {
        return publicPermitIf(trigger, representation.getUnderlyingState(), guard);
    }

    /// <summary>
    /// Ignore the specified trigger when in the configured state.
    /// </summary>
    /// <param name="trigger">The trigger to ignore.</param>
    /// <returns>The receiver.</returns>
    public StateConfiguration<TState, TTrigger> ignore(TTrigger trigger)
    {
        return ignoreIf(trigger, noGuard);
    }

    /// <summary>
    /// Ignore the specified trigger when in the configured state, if the guard
    /// returns true..
    /// </summary>
    /// <param name="trigger">The trigger to ignore.</param>
    /// <param name="guard">Function that must return true in order for the
    /// trigger to be ignored.</param>
    /// <returns>The receiver.</returns>
    public StateConfiguration<TState, TTrigger> ignoreIf(TTrigger trigger, Func<Boolean> guard)
    {
        ArgumentValidator.argumentNotNull(guard, "guard");
        representation.addTriggerBehaviour(new IgnoredTriggerBehaviour<TState, TTrigger>(trigger, guard));
        return this;
    }

    /// <summary>
    /// Specify an action that will execute when transitioning into
    /// the configured state.
    /// </summary>
    /// <param name="entryAction">Runnable to execute.</param>
    /// <returns>The receiver.</returns>
    public StateConfiguration<TState, TTrigger> onEntry(final Runnable entryAction)
    {
        ArgumentValidator.argumentNotNull(entryAction, "entryAction");

        representation.addEntryAction(entryAction);
        return this;

    }

    /// <summary>
    /// Specify an action that will execute when transitioning into
    /// the configured state.
    /// </summary>
    /// <param name="entryAction">Runnable to execute.</param>
    /// <param name="trigger">The trigger by which the state must be entered in order for the action to execute.</param>
    /// <returns>The receiver.</returns>
    public StateConfiguration<TState, TTrigger> onEntryFrom(TTrigger trigger, final Runnable entryAction)
    {
        ArgumentValidator.argumentNotNull(entryAction, "entryAction");

        representation.addEntryAction(trigger, entryAction);
        return this;

    }

    /// <summary>
    /// Specify an action that will execute when transitioning from
    /// the configured state.
    /// </summary>
    /// <param name="exitAction">Runnable to execute.</param>
    /// <returns>The receiver.</returns>
    public StateConfiguration<TState, TTrigger> onExit(final Runnable exitAction)
    {
        ArgumentValidator.argumentNotNull(exitAction, "exitAction");
        representation.addExitAction(exitAction);
        return this;

    }

    /// <summary>
    /// Sets the superstate that the configured state is a substate of.
    /// </summary>
    /// <remarks>
    /// Substates inherit the allowed transitions of their superstate.
    /// When entering directly into a substate from outside of the superstate,
    /// entry actions for the superstate are executed.
    /// Likewise when leaving from the substate to outside the supserstate,
    /// exit actions for the superstate will execute.
    /// </remarks>
    /// <param name="superstate">The superstate.</param>
    /// <returns>The receiver.</returns>
    public StateConfiguration<TState, TTrigger> substateOf(TState superstate) throws Exception
    {
        StateRepresentation<TState, TTrigger> superRepresentation = lookup.call(superstate);
        representation.setSuperstate(superRepresentation);
        superRepresentation.addSubstate(representation);
        return this;
    }

    /// <summary>
    /// Accept the specified trigger and transition to the destination state, calculated
    /// dynamically by the supplied function.
    /// </summary>
    /// <param name="trigger">The accepted trigger.</param>
    /// <param name="destinationStateSelector">Function to calculate the state 
    /// that the trigger will cause a transition to.</param>
    /// <returns>The reciever.</returns>
    public StateConfiguration<TState, TTrigger> permitDynamic(TTrigger trigger,
        final Func<TState> destinationStateSelector) throws Exception
    {
        return permitDynamicIf(trigger, destinationStateSelector, noGuard);
    }

    /// <summary>
    /// Accept the specified trigger and transition to the destination state, calculated
    /// dynamically by the supplied function.
    /// </summary>
    /// <param name="trigger">The accepted trigger.</param>
    /// <param name="destinationStateSelector">Function to calculate the state 
    /// that the trigger will cause a transition to.</param>
    /// <returns>The reciever.</returns>
    /// <typeparam name="TArg0">Type of the first trigger argument.</typeparam>
    public <TArg0> StateConfiguration<TState, TTrigger> permitDynamic(
        TriggerWithParameters1<TArg0, TState, TTrigger> trigger, Func2<TArg0, TState> destinationStateSelector)
        throws Exception
    {
        return permitDynamicIf(trigger, destinationStateSelector, noGuard);
    }

    /// <summary>
    /// Accept the specified trigger and transition to the destination state, calculated
    /// dynamically by the supplied function.
    /// </summary>
    /// <param name="trigger">The accepted trigger.</param>
    /// <param name="destinationStateSelector">Function to calculate the state 
    /// that the trigger will cause a transition to.</param>
    /// <returns>The reciever.</returns>
    /// <typeparam name="TArg0">Type of the first trigger argument.</typeparam>
    /// <typeparam name="TArg1">Type of the second trigger argument.</typeparam>
    public <TArg0, TArg1> StateConfiguration<TState, TTrigger> permitDynamic(
        TriggerWithParameters2<TArg0, TArg1, TState, TTrigger> trigger,
        Func3<TArg0, TArg1, TState> destinationStateSelector) throws Exception
    {
        return permitDynamicIf(trigger, destinationStateSelector, noGuard);
    }

    /// <summary>
    /// Accept the specified trigger and transition to the destination state, calculated
    /// dynamically by the supplied function.
    /// </summary>
    /// <param name="trigger">The accepted trigger.</param>
    /// <param name="destinationStateSelector">Function to calculate the state 
    /// that the trigger will cause a transition to.</param>
    /// <returns>The reciever.</returns>
    /// <typeparam name="TArg0">Type of the first trigger argument.</typeparam>
    /// <typeparam name="TArg1">Type of the second trigger argument.</typeparam>
    /// <typeparam name="TArg2">Type of the third trigger argument.</typeparam>
    public <TArg0, TArg1, TArg2> StateConfiguration<TState, TTrigger> permitDynamic(
        TriggerWithParameters3<TArg0, TArg1, TArg2, TState, TTrigger> trigger,
        final Func4<TArg0, TArg1, TArg2, TState> destinationStateSelector) throws Exception
    {
        return permitDynamicIf(trigger, destinationStateSelector, noGuard);
    }

    /// <summary>
    /// Accept the specified trigger and transition to the destination state, calculated
    /// dynamically by the supplied function.
    /// </summary>
    /// <param name="trigger">The accepted trigger.</param>
    /// <param name="destinationStateSelector">Function to calculate the state 
    /// that the trigger will cause a transition to.</param>
    /// <param name="guard">Function that must return true in order for the
    /// trigger to be accepted.</param>
    /// <returns>The reciever.</returns>
    public StateConfiguration<TState, TTrigger> permitDynamicIf(TTrigger trigger,
        final Func<TState> destinationStateSelector, Func<Boolean> guard) throws Exception
    {
        ArgumentValidator.argumentNotNull(destinationStateSelector, "destinationStateSelector");
        return publicPermitDynamicIf(trigger, new Func2<Object[], TState>()
        {

            public TState call(Object[] arg0)
            {
                return destinationStateSelector.call();
            }
        }, guard);
    }

    /// <summary>
    /// Accept the specified trigger and transition to the destination state, calculated
    /// dynamically by the supplied function.
    /// </summary>
    /// <param name="trigger">The accepted trigger.</param>
    /// <param name="destinationStateSelector">Function to calculate the state 
    /// that the trigger will cause a transition to.</param>
    /// <param name="guard">Function that must return true in order for the
    /// trigger to be accepted.</param>
    /// <returns>The reciever.</returns>
    /// <typeparam name="TArg0">Type of the first trigger argument.</typeparam>
    public <TArg0> StateConfiguration<TState, TTrigger> permitDynamicIf(
        TriggerWithParameters1<TArg0, TState, TTrigger> trigger,
        final Func2<TArg0, TState> destinationStateSelector, Func<Boolean> guard) throws Exception
    {
        ArgumentValidator.argumentNotNull(trigger, "trigger");
        ArgumentValidator.argumentNotNull(destinationStateSelector, "destinationStateSelector");
        return publicPermitDynamicIf(trigger.getTrigger(), new Func2<Object[], TState>()
        {
            @SuppressWarnings("unchecked")
            public TState call(Object[] args) throws Exception
            {
                return destinationStateSelector.call((TArg0) args[0]);

            }
        }, guard);
    }

    /// <summary>
    /// Accept the specified trigger and transition to the destination state, calculated
    /// dynamically by the supplied function.
    /// </summary>
    /// <param name="trigger">The accepted trigger.</param>
    /// <param name="destinationStateSelector">Function to calculate the state 
    /// that the trigger will cause a transition to.</param>
    /// <param name="guard">Function that must return true in order for the
    /// trigger to be accepted.</param>
    /// <returns>The reciever.</returns>
    /// <typeparam name="TArg0">Type of the first trigger argument.</typeparam>
    /// <typeparam name="TArg1">Type of the second trigger argument.</typeparam>
    public <TArg0, TArg1> StateConfiguration<TState, TTrigger> permitDynamicIf(
        TriggerWithParameters2<TArg0, TArg1, TState, TTrigger> trigger,
        final Func3<TArg0, TArg1, TState> destinationStateSelector, Func<Boolean> guard) throws Exception
    {
        ArgumentValidator.argumentNotNull(trigger, "trigger");
        ArgumentValidator.argumentNotNull(destinationStateSelector, "destinationStateSelector");
        return publicPermitDynamicIf(trigger.getTrigger(), new Func2<Object[], TState>()
        {
            @SuppressWarnings("unchecked")
            public TState call(Object[] args) throws Exception
            {
                return destinationStateSelector.call((TArg0) args[0], (TArg1) args[1]);
            }
        }, guard);
    }

    /// <summary>
    /// Accept the specified trigger and transition to the destination state, calculated
    /// dynamically by the supplied function.
    /// </summary>
    /// <param name="trigger">The accepted trigger.</param>
    /// <param name="destinationStateSelector">Function to calculate the state 
    /// that the trigger will cause a transition to.</param>
    /// <returns>The reciever.</returns>
    /// <param name="guard">Function that must return true in order for the
    /// trigger to be accepted.</param>
    /// <typeparam name="TArg0">Type of the first trigger argument.</typeparam>
    /// <typeparam name="TArg1">Type of the second trigger argument.</typeparam>
    /// <typeparam name="TArg2">Type of the third trigger argument.</typeparam>
    public <TArg0, TArg1, TArg2> StateConfiguration<TState, TTrigger> permitDynamicIf(
        TriggerWithParameters3<TArg0, TArg1, TArg2, TState, TTrigger> trigger,
        final Func4<TArg0, TArg1, TArg2, TState> destinationStateSelector, Func<Boolean> guard)
        throws Exception
    {
        ArgumentValidator.argumentNotNull(trigger, "trigger");
        ArgumentValidator.argumentNotNull(destinationStateSelector, "destinationStateSelector");
        return publicPermitDynamicIf(trigger.getTrigger(), new Func2<Object[], TState>()
        {
            @SuppressWarnings("unchecked")
            public TState call(Object[] args) throws Exception
            {
                return destinationStateSelector.call((TArg0) args[0], (TArg1) args[1], (TArg2) args[2]);
            }
        }, guard);
    }

    private void enforceNotIdentityTransition(TState destination)
    {
        if (destination.equals(representation.getUnderlyingState()))
        {
            throw new IllegalArgumentException("Destination cannot be the same as existing state.");
        }
    }

    private StateConfiguration<TState, TTrigger> publicPermit(TTrigger trigger, TState destinationState)
    {
        return publicPermitIf(trigger, destinationState, new Func<Boolean>()
        {

            public Boolean call()
            {
                return true;
            }
        });
    }

    private StateConfiguration<TState, TTrigger> publicPermitIf(TTrigger trigger, TState destinationState,
        Func<Boolean> guard)
    {
        ArgumentValidator.argumentNotNull(guard, "guard");
        representation.addTriggerBehaviour(new TransitioningTriggerBehaviour<TState, TTrigger>(trigger,
            destinationState, guard));
        return this;
    }

    private StateConfiguration<TState, TTrigger> publicPermitDynamicIf(TTrigger trigger,
        Func2<Object[], TState> destinationStateSelector, Func<Boolean> guard) throws Exception
    {
        ArgumentValidator.argumentNotNull(destinationStateSelector, "destinationStateSelector");
        ArgumentValidator.argumentNotNull(guard, "guard");
        representation.addTriggerBehaviour(new DynamicTriggerBehaviour<TState, TTrigger>(trigger,
            destinationStateSelector, guard));
        return this;
    }
}